<!DOCTYPE html>
<meta charset=utf-8>
<title>Verify timeline time, animation time, effect time and effect value for all fill modes in all timeline states: before start, at start, in range, at end, after end while using various effect delay values</title>
<meta name="timeout" content="long">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/web-animations/testcommon.js"></script>
<script src="testcommon.js"></script>
<style>
  .scroller {
    overflow: auto;
    height: 100px;
    width: 100px;
  }
  .contents {
    height: 1000px;
    width: 100%;
  }
</style>
<div id="log"></div>
<script>
  'use strict';

  // Test cases are included where effect delay causes the effect iteration to
  // overlap with the timeline start time and also the timeline end time.
  //                  Timeline
  //   BEFORE   +-----------------+     AFTER
  //   time:    0                 timeRange
  //   1)       +--------+
  //   2)            +-------+
  //   3)                      +------+
  //   4)   +------+

  // Each entry is [[test input], [test expectations]]
  // test input = ["description", delay, scroll percent]
  // test expectations = [timeline time, animation current time,
  //                      effect local time, effect progress, effect phase]
  const test_cases = [
    // Case 1: No delay. Effect starts at the same time as the timeline.
    [["before timeline start", 0, 0.1 ], [0,   0,   0,   null,  "before"]],
    [["at timeline start",     0, 0.2 ], [0,   0,   0,   0,     "active"]],
    [["in timeline range",     0, 0.5 ], [500, 500, 500, 0.833, "active"]],

    // Case 2: Positive delay.
    [["before timeline start", 100, 0.1 ], [0,      0,      0,      null,  "before"]],
    [["at timeline start",     100, 0.2 ], [0,      0,      0,      null,  "before"]],
    [["before effect delay",   100, 0.25], [83.333, 83.333, 83.333, null,  "before"]],
    [["at effect start",       100, 0.26], [100,    100,    100,    0,     "active"]],
    [["in timeline range",     100, 0.5 ], [500,    500,    500,    0.666, "active"]],
    [["at effect end",         100, 0.62], [700,    700,    700,    null,  "after"]],
    [["after effect end",      100, 0.65], [750,    700,    700,    null,  "after"]],
    [["at timeline end",       100, 0.8 ], [1000,   700,    700,    null,  "after"]],
    [["after timeline end",    100, 0.9 ], [1000,   700,    700,    null,  "after"]],

    // Case 3: Negative delay.
    // Can't test values for "before effect delay" and "at effect start" because
    // they occur before the timeline start and are therefore unreachable.
    [["before timeline start", -100, 0.1 ], [0,       0,       0,       null,  "before"]],
    [["at timeline start",     -100, 0.2 ], [0,       0,       0,       0.166, "active"]],
    [["in timeline range",     -100, 0.3 ], [166.666, 166.666, 166.666, 0.444, "active"]],
    [["at effect end",         -100, 0.5 ], [500,     500,     500,     null,  "after"]],
    [["after effect end",      -100, 0.51], [516.666, 500,     500,     null,  "after"]],
    [["at timeline end",       -100, 0.8 ], [1000,    500,     500,     null,  "after"]],
    [["after timeline end",    -100, 0.9 ], [1000,    500,     500,     null,  "after"]],

    // Case 4: Effect delay is large enough to cause the effect to not finish
    // before the timeline.
    [["before timeline start", 500, 0.1 ], [0,       0,       0,       null,  "before"]],
    [["at timeline start",     500, 0.2 ], [0,       0,       0,       null,  "before"]],
    [["before effect delay",   500, 0.4 ], [333.333, 333.333, 333.333, null,  "before"]],
    [["at effect start",       500, 0.5 ], [500,     500,     500,     0,     "active"]],
    [["in timeline range",     500, 0.65], [750,     750,     750,     0.416, "active"]],
    [["at timeline end",       500, 0.8 ], [1000,    1000,    1000,    0.833, "active"]],
    [["after timeline end",    500, 0.9 ], [1000,    1000,    1000,    0.833, "active"]],
    // Can't scroll past the end of the scroller and therefore cannot reach the
    // effect end, so "at effect end" and "after effect end" states are not
    // included.
  ];

  for (const test_case of test_cases) {
    const [inputs, expected] = test_case;
    const [test_name, delay, scroll_percentage] = inputs;

    const description = `Current times and effect phase ${test_name} when` +
      ` delay = ${delay} |`;

    promise_test(
      create_scroll_timeline_fill_test(delay, scroll_percentage, expected),
      description);
  }

  function create_scroll_timeline_fill_test(delay, scroll_percentage, expected){
    return async t => {
      const target = createDiv(t);
      const timeline = createScrollTimelineWithOffsets(t, CSS.percent(20), CSS.percent(80));
      const effect = new KeyframeEffect(
        target,
        {
          opacity: [0.3, 0.7]
        },
        {
          duration: 600,
          delay: delay
        }
      );
      const animation = new Animation(effect, timeline);
      const scroller = timeline.scrollSource;
      const maxScroll = scroller.scrollHeight - scroller.clientHeight;

      animation.play();

      await animation.ready;

      scroller.scrollTop = scroll_percentage * maxScroll;

      // Wait for new animation frame which allows the timeline to compute
      // new current time.
      await waitForNextFrame();

      const [expected_timeline_current_time,
        expected_animation_current_time,
        expected_effect_local_time,
        expected_effect_progress,
        expected_effect_phase] = expected;

      assert_times_equal(
        animation.timeline.currentTime,
        expected_timeline_current_time,
        "timeline current time"
      );
      assert_times_equal(
        animation.currentTime,
        expected_animation_current_time,
        "animation current time"
      );
      assert_times_equal(
        animation.effect.getComputedTiming().localTime,
        expected_effect_local_time,
        "animation effect local time"
      );
      assert_approx_equals_or_null(
        animation.effect.getComputedTiming().progress,
        expected_effect_progress,
        0.001,
        "animation effect progress"
      );
      assert_phase_at_time(
        animation,
        expected_effect_phase,
        animation.currentTime
      );
    }
  }

  function createKeyframeEffectOpacity(test){
    return new KeyframeEffect(
      createDiv(test),
      {
        opacity: [0.3, 0.7]
      },
      {
        duration: 1000
      }
    );
  }

  function verifyTimelineBeforePhase(animation){
    assert_equals(animation.timeline.phase, "before");
    assert_equals(animation.timeline.currentTime, 0);
    assert_equals(animation.currentTime, 0);
    assert_equals(
      animation.effect.getComputedTiming().localTime,
      0,
      "effect local time in timeline before phase");
  }

  function verifyEffectBeforePhase(animation){
    // progress == null AND opacity == 1 implies we are in the effect before
    // phase
    assert_equals(
      animation.effect.getComputedTiming().progress,
      null
    );
    assert_equals(
      window.getComputedStyle(animation.effect.target).getPropertyValue("opacity"),
      "1"
    );
  }

  promise_test(async t => {
    const animation = new Animation(
      createKeyframeEffectOpacity(t),
      createScrollTimelineWithOffsets(t, CSS.percent(20), CSS.percent(80))
    );

    const scroller = animation.timeline.scrollSource;
    const maxScroll = scroller.scrollHeight - scroller.clientHeight;

    animation.play();
    await animation.ready;

    verifyTimelineBeforePhase(animation);
    verifyEffectBeforePhase(animation);

    animation.pause();
    await waitForNextFrame();

    verifyTimelineBeforePhase(animation);
    verifyEffectBeforePhase(animation);

    animation.play();
    await waitForNextFrame();

    verifyTimelineBeforePhase(animation);
    verifyEffectBeforePhase(animation);
  }, 'Verify that (play -> pause -> play) doesn\'t change phase/progress.');

  promise_test(async t => {
    const animation = new Animation(
      createKeyframeEffectOpacity(t),
      createScrollTimelineWithOffsets(t, CSS.percent(20), CSS.percent(80))
    );

    const scroller = animation.timeline.scrollSource;
    const maxScroll = scroller.scrollHeight - scroller.clientHeight;

    animation.play();
    await animation.ready;

    verifyTimelineBeforePhase(animation);
    verifyEffectBeforePhase(animation);

    animation.pause();
    await waitForNextFrame();

    verifyTimelineBeforePhase(animation);
    verifyEffectBeforePhase(animation);

    // Scrolling should not cause the animation effect to change.
    scroller.scrollTop = 0.5 * maxScroll;
    await waitForNextFrame();

    // Check timeline phase
    assert_equals(animation.timeline.phase, "active");
    assert_equals(animation.timeline.currentTime, 500);
    assert_equals(animation.currentTime, 0);
    assert_equals(
      animation.effect.getComputedTiming().localTime,
      0,
      "effect local time"
    );

    // Make sure the effect is still in the before phase even though the
    // timeline is not.
    verifyEffectBeforePhase(animation);
  }, 'Pause in before phase, scroll timeline into active phase, animation ' +
  'should remain in the before phase');

promise_test(async t => {
  const animation = new Animation(
    createKeyframeEffectOpacity(t),
    createScrollTimelineWithOffsets(t, CSS.percent(20), CSS.percent(80))
  );
  const scroller = animation.timeline.scrollSource;
  const maxScroll = scroller.scrollHeight - scroller.clientHeight;

  animation.play();
  await animation.ready;

  // Causes the timeline to be inactive
  scroller.style.overflow = "visible";
  await waitForNextFrame();
  await waitForNextFrame();

  // Check timeline phase
  assert_equals(animation.timeline.phase, "inactive");
  assert_equals(animation.timeline.currentTime, null);
  assert_equals(animation.currentTime, null);
  assert_equals(
    animation.effect.getComputedTiming().localTime,
    null,
    "effect local time with inactive timeline"
  );

  verifyEffectBeforePhase(animation);

  // Setting the current time while timeline is inactive should cause hold phase
  // and hold time to be populated
  animation.currentTime = 500;
  await waitForNextFrame();
  await waitForNextFrame();

  // Check timeline phase
  assert_equals(animation.timeline.phase, "inactive");
  assert_equals(animation.timeline.currentTime, null);
  assert_equals(animation.currentTime, 500);
  assert_equals(
    animation.effect.getComputedTiming().localTime,
    500,
    "effect local time after setting animation current time"
  );

  // Check effect phase
  // progress == 0.5 AND opacity == 0.5 shows we are in the effect active phase
  assert_equals(
    animation.effect.getComputedTiming().progress,
    0.5,
    "effect progress"
  );
  assert_equals(
    window.getComputedStyle(animation.effect.target).getPropertyValue("opacity"),
    "0.5",
    "effect opacity after setting animation current time"
  );
}, 'Make scroller inactive, then set current time to an in range time');

promise_test(async t => {
  const animation = new Animation(
    createKeyframeEffectOpacity(t),
    createScrollTimelineWithOffsets(t, CSS.percent(20), CSS.percent(80))
  );
  const scroller = animation.timeline.scrollSource;
  const maxScroll = scroller.scrollHeight - scroller.clientHeight;
  scroller.scrollTop = 0.5 * maxScroll;
  // allow the scroll to finish.
  await waitForNextFrame();

  animation.pause();
  await animation.ready;
  // verify effect is applied.
  const expected_progress = 0.5;
  assert_equals(
    animation.effect.getComputedTiming().progress,
    expected_progress,
    "Verify effect progress after pausing."
  );

  // cause the timeline to become inactive
  scroller.style.overflow = 'visible';
  await waitForAnimationFrames(2);
  assert_equals(animation.timeline.currentTime, null,
    'Sanity check the timeline is inactive.');
  assert_equals(
    animation.effect.getComputedTiming().progress,
    expected_progress,
    "Verify effect progress after the timeline goes inactive."
  );
}, 'Animation effect is still applied after pausing and making timeline ' +
   'inactive.');

</script>